﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Xml.Linq;
using System.Xml.Schema;

namespace Framework.Core
{
    [ExcludeFromCodeCoverage]
    public static class LinqToXmlPatchExtension
    {
        public static XDocument Normalize(this XDocument source, XmlSchemaSet schema = null)
        {
            var havePsvi = false;
            if (schema != null)
            {
                source.Validate(schema, null, true);
                havePsvi = true;
            }
            return new XDocument(
                source.Declaration,
                source.Nodes().Select(n =>
                    {
                        if (n is XComment || n is XProcessingInstruction || n is XText)
                        {
                            return null;
                        }
                        var e = n as XElement;
                        return e != null ? NormalizeElement(e, havePsvi) : n;
                    })
                );
        }

        public static bool DeepEqualsWithNormalization(this XDocument doc1, XDocument doc2,
                                                       XmlSchemaSet schemaSet = null)
        {
            var d1 = Normalize(doc1, schemaSet);
            var d2 = Normalize(doc2, schemaSet);
            return XNode.DeepEquals(d1, d2);
        }

        private static IEnumerable<XAttribute> NormalizeAttributes(XElement element,
                                                                   bool havePsvi)
        {
            return element.Attributes()
                .Where(a => !a.IsNamespaceDeclaration &&
                            a.Name != Xsi.SchemaLocation &&
                            a.Name != Xsi.NoNamespaceSchemaLocation)
                .OrderBy(a => a.Name.NamespaceName)
                .ThenBy(a => a.Name.LocalName)
                .Select(
                    a =>
                        {
                            if (havePsvi)
                            {
                                // ReSharper disable PossibleNullReferenceException
                                var dt = a.GetSchemaInfo().SchemaType.TypeCode;
                                // ReSharper restore PossibleNullReferenceException
                                switch (dt)
                                {
                                    case XmlTypeCode.Boolean:
                                        return new XAttribute(a.Name, (bool) a);
                                    case XmlTypeCode.DateTime:
                                        return new XAttribute(a.Name, (DateTime) a);
                                    case XmlTypeCode.Decimal:
                                        return new XAttribute(a.Name, (decimal) a);
                                    case XmlTypeCode.Double:
                                        return new XAttribute(a.Name, (double) a);
                                    case XmlTypeCode.Float:
                                        return new XAttribute(a.Name, (float) a);
                                    case XmlTypeCode.HexBinary:
                                    case XmlTypeCode.Language:
                                        return new XAttribute(a.Name,
                                                              ((string) a).ToLower());
                                }
                            }
                            return a;
                        }
                );
        }

        private static XNode NormalizeNode(XNode node, bool havePsvi)
        {
            if (node is XComment || node is XProcessingInstruction)
            {
                return null;
            }
            var e = node as XElement;
            return e != null ? NormalizeElement(e, havePsvi) : node;
        }

        private static XElement NormalizeElement(XElement element, bool havePsvi)
        {
            if (havePsvi)
            {
                var dt = element.GetSchemaInfo();
                // ReSharper disable PossibleNullReferenceException
                switch (dt.SchemaType.TypeCode)
                    // ReSharper restore PossibleNullReferenceException
                {
                    case XmlTypeCode.Boolean:
                        return new XElement(element.Name,
                                            NormalizeAttributes(element, true),
                                            (bool) element);
                    case XmlTypeCode.DateTime:
                        return new XElement(element.Name,
                                            NormalizeAttributes(element, true),
                                            (DateTime) element);
                    case XmlTypeCode.Decimal:
                        return new XElement(element.Name,
                                            NormalizeAttributes(element, true),
                                            (decimal) element);
                    case XmlTypeCode.Double:
                        return new XElement(element.Name,
                                            NormalizeAttributes(element, true),
                                            (double) element);
                    case XmlTypeCode.Float:
                        return new XElement(element.Name,
                                            NormalizeAttributes(element, true),
                                            (float) element);
                    case XmlTypeCode.HexBinary:
                    case XmlTypeCode.Language:
                        return new XElement(element.Name,
                                            NormalizeAttributes(element, true),
                                            ((string) element).ToLower());
                    default:
                        return new XElement(element.Name,
                                            NormalizeAttributes(element, true),
                                            element.Nodes().Select(n => NormalizeNode(n, true))
                            );
                }
            }
            return new XElement(element.Name,
                                NormalizeAttributes(element, false),
                                element.Nodes().Select(n => NormalizeNode(n, false))
                );
        }

        private static class Xsi
        {
            private static readonly XNamespace NameSpace = "http://www.w3.org/2001/XMLSchema-instance";

            public static readonly XName SchemaLocation = NameSpace + "schemaLocation";
            public static readonly XName NoNamespaceSchemaLocation = NameSpace + "noNamespaceSchemaLocation";
        }
    }
}